5.08. ООП в Smalltalk
ООП в Smalltalk
Пример класса
"Определение класса"
Object subclass: #Unit
instanceVariableNames: 'name intel agility strength health mana level'
classVariableNames: ''
poolDictionaries: ''
category: 'Game-Entities'
"Метод инициализации"
Unit>>initialize
name := 'Имя'.
intel := 10.
agility := 10.
strength := 10.
health := 100.
mana := 50.
level := 1.
^self
"Метод вычисления урона"
Unit>>damage
^intel + agility + strength + (level * 2)
"Метод атаки"
Unit>>attack: target
| damageValue |
damageValue := self damage.
Transcript show: name, ' атакует ', target name, ' и наносит ', damageValue printString, ' единиц урона.'; cr.
target health: (target health - damageValue).
Transcript show: target name, ' теперь имеет ', (target health) printString, ' здоровья.'; cr.
"Сеттеры и геттеры для здоровья"
Unit>>health
^health
Unit>>health: newHealth
health := newHealth
"Создание объектов и демонстрация взаимодействия"
| warrior mage |
warrior := Unit new initialize.
warrior instVarAt: 1 put: 'Воин'.
warrior instVarAt: 2 put: 5. "intel"
warrior instVarAt: 3 put: 15. "agility"
warrior instVarAt: 4 put: 30. "strength"
mage := Unit new initialize.
mage instVarAt: 1 put: 'Маг'.
mage instVarAt: 2 put: 35. "intel"
mage instVarAt: 3 put: 10. "agility"
mage instVarAt: 4 put: 5. "strength"
warrior attack: mage.
mage attack: warrior.
Выражение Object subclass: #Unit создаёт новый класс как подкласс базового класса Object. Параметр instanceVariableNames перечисляет имена переменных экземпляра через пробел. Каждая переменная экземпляра принадлежит отдельному объекту и хранит независимое значение. Параметр category определяет категорию класса в системном браузере.
Метод initialize вызывается автоматически после создания объекта сообщением new. В методе устанавливаются начальные значения всех переменных экземпляра через оператор присваивания :=. Возврат ^self возвращает текущий объект для возможной цепочки вызовов.
Метод damage возвращает результат арифметического выражения. Оператор ^ указывает возврат значения. Выражение складывает характеристики персонажа и добавляет бонус уровня. Каждый вызов метода производит актуальный расчёт на основе текущего состояния объекта.
Метод attack: принимает целевой объект через параметр target. Локальная переменная damageValue сохраняет результат вычисления урона для повторного использования. Объект Transcript представляет системное окно вывода. Сообщение show: выводит текст, сообщение cr добавляет символ новой строки. Метод изменяет состояние цели через сеттер health:.
Геттер health возвращает текущее значение здоровья. Сеттер health: принимает новое значение и сохраняет его в переменную экземпляра. Такой подход обеспечивает контролируемый доступ к внутреннему состоянию объекта.
Сообщение new создаёт новый экземпляр класса. Сообщение initialize устанавливает начальные значения. Для изменения характеристик используется низкоуровневый метод instVarAt:put:, который напрямую устанавливает значение переменной экземпляра по её порядковому номеру в объявлении класса.
Объекты взаимодействуют через посылку сообщений. Выражение warrior attack: mage посылает сообщение attack: объекту warrior с аргументом mage. Объект-получатель самостоятельно определяет, как обработать сообщение, реализуя соответствующий метод. Такой механизм составляет основу объектно-ориентированного взаимодействия в Smalltalk.
Программа выполняется в интерактивной среде Smalltalk. Окно Transcript отображает результаты вывода. Каждое выражение можно выполнять по отдельности в рабочем пространстве, что позволяет пошагово исследовать поведение объектов и их взаимодействие.
ООП в Smalltalk
Почему ST «чистое ООП»? Потому что нет функций, операторов, процедур. Только объекты. Поэтому и ОБЪЕКТНО-ориентированное программирование.
Помните основы ООП?
Наследование, полиморфизм, абстракция и инкапсуляция, так?
Класс — это шаблон для создания объектов.
В Smalltalk класс определяется как объект, отвечающий за создание и поведение своих экземпляров. То есть, в этом языке класс сам является объектом.
Создание класса:
Object subclass: #Person
instanceVariableNames: 'name age'
classVariableNames: ''
package: 'MyApp'
Здесь:
Object— родительский класс.#Person— имя нового класса.'name age'— экземплярные переменные (приватные).package: 'MyApp'— пространство имён.
Создание объекта (экземпляра):
| person |
person := Person new.
new — это сообщение, отправленное классу Person.
Причем, класс Person — это объект, экземпляр своего метакласса. Вы можете:
Person class "→ Metaclass of Person"
Person superclass "→ Object"
Person methods "→ список всех методов"
Person numberOfInstances "→ сколько объектов создано"
Именно так и можно в идеале реализовывать паттерны вроде Singleton, фабрики, рефлексию — прямо в языке. И всё что есть у класса, можно посмотреть прямо в инспекторе, как мы видели в Pharo.
Метод — это реализация ответа на сообщение.
Пример метода:
getName
^ name
setName: aName
name := aName
introduceYourself
^ 'Hi, I am ', name, ' and I am ', age printString, ' years old.'
^ — возврат значения (аналог return). Мы именно так и создали наш метод, который выводил (возвращал) Hello World!
setName: aName — ключевое сообщение с одним параметром.
Методы не принадлежат объекту напрямую — они хранятся в классе. Объект знает, где искать метод — по цепочке наследования.
Говоря о наследовании. Smalltalk поддерживает одиночное наследование. Нет множественного наследования — но и не нужно.
Object subclass: #Animal
instanceVariableNames: 'name'
...
Animal subclass: #Dog
instanceVariableNames: 'breed'
...
Теперь Dog наследует все переменные и методы от Animal.
Помните про переопределение? В ST тоже есть переопределение метода:
Dog >> makeSound
^ 'Woof!'
Вызов родительского метода:
Dog >> introduce
^ super introduce, ' I am a dog.'
super — специальное ключевое слово, указывающее на родительский класс.
Вместо этого — композиция, делегирование, поведение через блоки.
А как с инкапсуляцией?
В Smalltalk экземплярные переменные — приватны. Доступ только через методы. Методы — публичные, если не помечены иначе (в некоторых реализациях можно указать private). Нет ключевых слов private, protected, public — но дисциплина поддерживается соглашениями.
Person >> name
^ name
Person >> name: aName
name := aName
Код за пределами класса не должен напрямую читать или менять name — только через геттер и сеттер. Инкапсуляция достигается договорённостью и культурой, а не синтаксисом.
Полиморфизм в Smalltalk — естественное следствие посылки сообщений. Если разные объекты понимают одно и то же сообщение, они могут использоваться взаимозаменяемо.
Object subclass: #Shape
...
Shape subclass: #Circle
...
Shape subclass: #Rectangle
...
Оба класса реализуют:
Circle >> area
^ 3.14 * radius * radius
Rectangle >> area
^ width * height
Теперь можно писать:
| shapes total |
shapes := { Circle new. Rectangle new }.
total := 0.
shapes do: [ :each | total := total + each area ].
Никаких интерфейсов, аннотаций, типов.
Полиморфизм — duck typing: «если крякает как утка, плавает как утка — значит, это утка».
Интерфейсов и абстрактных классов в ST нет, ибо нет понятий interface или abstract class, как в Java или C#. Потому что они не нужны. Вместо интерфейсов — соглашения о сообщениях. Если объект отвечает на draw, move, area — он "реализует" этот «интерфейс». Это динамический полиморфизм, более гибкий, чем статические интерфейсы.
Вот такое вот ООП. Вроде бы изучали столько всего, а на практике в Smalltalk не так уж и много деталей, не правда ли?
Но на самом деле это лишь основы, ведь язык был одним из первых. Технологии идут вперёд, языки появляются и развиваются. Теперь давайте наконец начнём изучать более современные языки.